vue滑块验证,前端+后端代码及逻辑

您所在的位置:网站首页 Vue 前端验证码代码 vue滑块验证,前端+后端代码及逻辑

vue滑块验证,前端+后端代码及逻辑

2023-09-25 17:43| 来源: 网络整理| 查看: 265

最近总部的漏洞扫描团队天天对我们的系统进行SQL注入,登陆破解..等等,搞得表示压力山大,其中对我们 的滑块验证码存在绕过登陆问题发报,最终把账号、密码登陆下线,今天终于把这块的bug进行修复了,先看效果:

第一步:弹出框架子模块_src/components/loginmove/slide-verify.vue

container --> {{sliderText}} const PI = Math.PI; function sum(x, y) { return x + y } function square(x) { return x * x } export default { name: 'SlideVerify', props: { // block length l: { type: Number, default: 42, }, // block radius r: { type: Number, default: 10, }, // canvas width w: { type: Number, default: 310, }, // canvas height h: { type: Number, default: 155, }, block_x: { type: Number, default: 155, }, block_y: { type: Number, default: 20, }, sliderText: { type: String, default: 'Slide filled right', }, imgurl: { type: String, default: '', }, miniimgurl: { type: String, default: '', }, }, data() { return { containerActive: false, // container active class containerSuccess: false, // container success class containerFail: false, // container fail class canvasCtx: null, blockCtx: null, block: null, canvasStr: null, //block_x: undefined, // container random position // block_y: undefined, L: this.l + this.r * 2 + 3, // block real lenght img: undefined, originX: undefined, originY: undefined, isMouseDown: false, trail: [], widthlable:'', sliderLeft: 0, // block right offset sliderMaskWidth: 0, // mask width } }, mounted() { //随机生成数this.block_x = this.init() }, methods: { init() { this.initDom() this.bindEvents() this.widthlable ='width:'+this.w+'px;' }, initDom() { this.block = this.$refs.block; this.canvasStr = this.$refs.canvas; this.canvasCtx = this.canvasStr.getContext('2d') this.blockCtx = this.block.getContext('2d') }, initImg(h) { var that = this const img = document.createElement('img'); img.onload = onload; img.onerror = () => { img.src = that.imgurl } img.src = that.imgurl img.onload = function(){ that.canvasCtx.drawImage(img, 0, 0, that.w, that.h) } this.img=img const img1 = document.createElement('img'); var blockCtx = that.blockCtx var block_y = h||that.block_y img1.onerror = () => { img1.src = that.miniimgurl } img1.src = that.miniimgurl img1.onload = function(){ blockCtx.drawImage(img1, 0, block_y, 61, 61) } }, refresh() { this.$emit('refresh') }, sliderDown(event) { this.originX = event.clientX; this.originY = event.clientY; this.isMouseDown = true; }, touchStartEvent(e) { this.originX = e.changedTouches[0].pageX; this.originY = e.changedTouches[0].pageY; this.isMouseDown = true; }, bindEvents() { document.addEventListener('mousemove', (e) => { if (!this.isMouseDown) return false; const moveX = e.clientX - this.originX; const moveY = e.clientY - this.originY; if (moveX < 0 || moveX + 38 >= this.w) return false; this.sliderLeft = moveX + 'px'; let blockLeft = (this.w - 40 - 20) / (this.w - 40) * moveX; this.block.style.left = blockLeft + 'px'; this.containerActive = true; // add active this.sliderMaskWidth = moveX + 'px'; this.trail.push(moveY); }); document.addEventListener('mouseup', (e) => { if (!this.isMouseDown) return false this.isMouseDown = false if (e.clientX === this.originX) return false; this.containerActive = false; // remove active this.verify() }) }, touchMoveEvent(e) { if (!this.isMouseDown) return false; const moveX = e.changedTouches[0].pageX - this.originX; const moveY = e.changedTouches[0].pageY - this.originY; if (moveX < 0 || moveX + 38 >= this.w) return false; this.sliderLeft = moveX + 'px'; let blockLeft = (this.w - 40 - 20) / (this.w - 40) * moveX; this.block.style.left = blockLeft + 'px'; this.containerActive = true; this.sliderMaskWidth = moveX + 'px'; this.trail.push(moveY); }, touchEndEvent(e) { if (!this.isMouseDown) return false this.isMouseDown = false if (e.changedTouches[0].pageX === this.originX) return false; this.containerActive = false; this.verify() }, verify() { const arr = this.trail // drag y move distance const average = arr.reduce(sum) / arr.length // average const deviations = arr.map(x => x - average) // deviation array const stddev = Math.sqrt(deviations.map(square).reduce(sum) / arr.length) // standard deviation const left = parseInt(this.block.style.left) this.$emit('success',left) }, reset(h) { this.containerActive = false; this.containerSuccess = false; this.containerFail = false; this.sliderLeft = 0; this.block.style.left = 0; this.sliderMaskWidth = 0; this.canvasCtx.clearRect(0, 0, this.w, this.h) this.blockCtx.clearRect(0, 0, this.w,this.h) this.initImg(h) } } } .slide-verify { position: relative; width: 310px; } .slide-verify-block { position: absolute; left: 0; top: 0 } .slide-verify-refresh-icon { position: absolute; right: 0; top: 0; width: 34px; height: 34px; cursor: pointer; background: url("../../assets/move/icon_light.png") 0 -437px; background-size: 34px 471px } .slide-verify-slider { position: relative; text-align: center; width: 310px; height: 40px; line-height: 40px; margin-top: 15px; background: #f7f9fa; color: #45494c; border: 1px solid #e4e7eb } .slide-verify-slider-mask { position: absolute; left: 0; top: 0; height: 40px; border: 0 solid #1991FA; background: #D1E9FE } .slide-verify-slider-mask-item { position: absolute; top: 0; left: 0; width: 40px; height: 40px; background: #fff; box-shadow: 0 0 3px rgba(0, 0, 0, 0.3); cursor: pointer; transition: background .2s linear } .slide-verify-slider-mask-item:hover { background: #1991FA } .slide-verify-slider-mask-item:hover .slide-verify-slider-mask-item-icon { background-position: 0 -13px } .slide-verify-slider-mask-item-icon { position: absolute; top: 15px; left: 13px; width: 14px; height: 12px; background: url("../../assets/move/icon_light.png") 0 -26px; background-size: 34px 471px } .container-active .slide-verify-slider-mask-item { height: 38px; top: -1px; border: 1px solid #1991FA; } .container-active .slide-verify-slider-mask { height: 38px; border-width: 1px; } .container-success .slide-verify-slider-mask-item { height: 38px; top: -1px; border: 1px solid #52CCBA; background-color: #52CCBA !important; } .container-success .slide-verify-slider-mask { height: 38px; border: 1px solid #52CCBA; background-color: #D2F4EF; } .container-success .slide-verify-slider-mask-item-icon { background-position: 0 0 !important; } .container-fail .slide-verify-slider-mask-item { height: 38px; top: -1px; border: 1px solid #f57a7a; background-color: #f57a7a !important; } .container-fail .slide-verify-slider-mask { height: 38px; border: 1px solid #f57a7a; background-color: #fce1e1; } .container-fail .slide-verify-slider-mask-item-icon { top: 14px; background-position: 0 -82px !important; } .container-active .slide-verify-slider-text, .container-success .slide-verify-slider-text, .container-fail .slide-verify-slider-text { display: none; }

在登陆中引入:

import slideverify from '@/components/loginmove/slide-verify.vue'

获取图片验证码:

getImageVerifyCode: function() { var that = this loginApi.getImageVerifyCode().then(res => { if (!res || res.code != '20000') { this.$message({ showClose: true, message: '获取图形验证码失败,请重试', type: 'error' }) } else { var imgobj = JSON.parse(res.data) that.block_y = imgobj.yHeight that.imgurl = 'data:image/png;base64,' + imgobj.bigImage that.miniimgurl = 'data:image/png;base64,' + imgobj.smallImage that.chenckMoveid = imgobj.chenckMoveid if (that.$refs.dialogopen) { that.$refs.dialogopen.reset(imgobj.yHeight) } } }) }, onRefresh() { this.imgurl = '' this.miniimgurl = '' this.getImageVerifyCode() },, /* 用户密码登陆方法*/ submitForm(formName,data) { var that = this const loading = this.$loading({ lock: true, text: '登录中...', spinner: 'el-icon-loading', background: 'rgba(0, 0, 0, 0.7)' }) this.loading = true /* 发起请求*/ let Base64 = require('js-base64').Base64 const loginFrom = { username: Base64.encode(this.ruleForm2.username), password: Base64.encode(this.ruleForm2.password), x_index: data, chenckMoveid: that.chenckMoveid } this.$store.dispatch('Login', loginFrom).then(res=> { loading.close() this.isLogined = true this.loading = false /* 跳转路由*/ this.$router.push({ path: '/' }) }).catch(() => { loading.close() this.loading = false if (that.$refs.dialogopen) { that.$refs.dialogopen.reset() } }) },

注意:由于采用了是前后端分离的方式,考虑到cookie和session比较麻烦,因此每次获取滑块图片的时候生成一个uuid保存到redis里头,作为key进行保存对应的横坐标。

第二步:后台java代码

生成滑块及图片抠图工具类

package com.app.auth.api.util; import java.awt.BasicStroke; import java.awt.Color; import java.awt.Graphics2D; import java.awt.RenderingHints; import java.awt.Transparency; import java.awt.image.BufferedImage; import java.io.ByteArrayOutputStream; import java.io.File; import java.util.HashMap; import java.util.Map; import java.util.Random; import javax.imageio.ImageIO; import org.springframework.util.Base64Utils; public class VerifyImageUtil { // private static Logger log = LoggerFactory.getLogger(VerifyImageUtil.class); private static int BOLD = 5; private static final String IMG_FILE_TYPE = "jpg"; private static final String TEMP_IMG_FILE_TYPE = "png"; /** * 根据模板切图 * @param templateFile * @param targetFile * @return * @throws Exception */ public static Map pictureTemplatesCut(File templateFile, File targetFile) throws Exception { Map pictureMap = new HashMap(); // 模板图 BufferedImage imageTemplate = ImageIO.read(templateFile); int templateWidth = imageTemplate.getWidth(); int templateHeight = imageTemplate.getHeight(); // 原图 BufferedImage oriImage = ImageIO.read(targetFile); int oriImageWidth = oriImage.getWidth(); int oriImageHeight = oriImage.getHeight(); //随机生成抠图坐标X,Y //X轴距离右端targetWidth Y轴距离底部targetHeight以上 Random random = new Random(); int widthRandom = random.nextInt(oriImageWidth - 2*templateWidth) + templateWidth; int heightRandom = random.nextInt(oriImageHeight - templateHeight); // log.info("原图大小{} x {},随机生成的坐标 X,Y 为({},{})", oriImageWidth,oriImageHeight,widthRandom,heightRandom); // 新建一个和模板一样大小的图像,TYPE_4BYTE_ABGR表示具有8位RGBA颜色分量的图像,正常取imageTemplate.getType() BufferedImage newImage = new BufferedImage(templateWidth, templateHeight, imageTemplate.getType()); //得到画笔对象 Graphics2D graphics = newImage.createGraphics(); //如果需要生成RGB格式,需要做如下配置,Transparency 设置透明 newImage = graphics.getDeviceConfiguration().createCompatibleImage(templateWidth, templateHeight, Transparency.TRANSLUCENT); // 新建的图像根据模板颜色赋值,源图生成遮罩 cutByTemplate(oriImage,imageTemplate,newImage,widthRandom,heightRandom); // 设置“抗锯齿”的属性 graphics.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON); graphics.setStroke(new BasicStroke(BOLD, BasicStroke.CAP_BUTT, BasicStroke.JOIN_BEVEL)); graphics.drawImage(newImage, 0, 0, null); graphics.dispose(); ByteArrayOutputStream newImageOs = new ByteArrayOutputStream();//新建流。 ImageIO.write(newImage, TEMP_IMG_FILE_TYPE, newImageOs);//利用ImageIO类提供的write方法,将bi以png图片的数据模式写入流。 byte[] newImagebyte = newImageOs.toByteArray(); ByteArrayOutputStream oriImagesOs = new ByteArrayOutputStream();//新建流。 ImageIO.write(oriImage, IMG_FILE_TYPE, oriImagesOs);//利用ImageIO类提供的write方法,将bi以jpg图片的数据模式写入流。 byte[] oriImageByte = oriImagesOs.toByteArray(); pictureMap.put("smallImage", Base64Utils.encodeToString(newImagebyte)); pictureMap.put("bigImage", Base64Utils.encodeToString(oriImageByte)); System.out.println("widthRandom:"+widthRandom); pictureMap.put("xWidth",widthRandom); pictureMap.put("yHeight",heightRandom); return pictureMap; } /** * 添加水印 * @param oriImage */ /*private static BufferedImage addWatermark(BufferedImage oriImage) throws IOException { Graphics2D graphics2D = oriImage.createGraphics(); graphics2D.setRenderingHint(RenderingHints.KEY_INTERPOLATION,RenderingHints.VALUE_INTERPOLATION_BILINEAR); // 设置水印文字颜色 graphics2D.setColor(Color.BLUE); // 设置水印文字Font graphics2D.setFont(new java.awt.Font("宋体", java.awt.Font.BOLD, 50)); // 设置水印文字透明度 graphics2D.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC_ATOP, 0.5f)); // 第一参数->设置的内容,后面两个参数->文字在图片上的坐标位置(x,y) graphics2D.drawString("[email protected]", 400,300); graphics2D.dispose(); //释放 return oriImage; }*/ /** * @param oriImage 原图 * @param templateImage 模板图 * @param newImage 新抠出的小图 * @param x 随机扣取坐标X * @param y 随机扣取坐标y * @throws Exception */ private static void cutByTemplate(BufferedImage oriImage, BufferedImage templateImage,BufferedImage newImage, int x, int y){ //临时数组遍历用于高斯模糊存周边像素值 int[][] martrix = new int[3][3]; int[] values = new int[9]; int xLength = templateImage.getWidth(); int yLength = templateImage.getHeight(); // 模板图像宽度 for (int i = 0; i < xLength; i++) { // 模板图片高度 for (int j = 0; j < yLength; j++) { // 如果模板图像当前像素点不是透明色 copy源文件信息到目标图片中 int rgb = templateImage.getRGB(i, j); if (rgb < 0) { newImage.setRGB(i, j,oriImage.getRGB(x + i, y + j)); //抠图区域高斯模糊 readPixel(oriImage, x + i, y + j, values); fillMatrix(martrix, values); oriImage.setRGB(x + i, y + j, avgMatrix(martrix)); } //防止数组越界判断 if(i == (xLength-1) || j == (yLength-1)){ continue; } int rightRgb = templateImage.getRGB(i + 1, j); int downRgb = templateImage.getRGB(i, j + 1); //描边处理,,取带像素和无像素的界点,判断该点是不是临界轮廓点,如果是设置该坐标像素是白色 if((rgb >= 0 && rightRgb < 0) || (rgb < 0 && rightRgb >= 0) || (rgb >= 0 && downRgb < 0) || (rgb < 0 && downRgb >= 0)){ newImage.setRGB(i, j, Color.white.getRGB()); oriImage.setRGB(x + i, y + j,Color.white.getRGB()); } } } } private static void readPixel(BufferedImage img, int x, int y, int[] pixels) { int xStart = x - 1; int yStart = y - 1; int current = 0; for (int i = xStart; i < 3 + xStart; i++) for (int j = yStart; j < 3 + yStart; j++) { int tx = i; if (tx < 0) { tx = -tx; } else if (tx >= img.getWidth()) { tx = x; } int ty = j; if (ty < 0) { ty = -ty; } else if (ty >= img.getHeight()) { ty = y; } pixels[current++] = img.getRGB(tx, ty); } } private static void fillMatrix(int[][] matrix, int[] values) { int filled = 0; for (int i = 0; i < matrix.length; i++) { int[] x = matrix[i]; for (int j = 0; j < x.length; j++) { x[j] = values[filled++]; } } } private static int avgMatrix(int[][] matrix) { int r = 0; int g = 0; int b = 0; for (int i = 0; i < matrix.length; i++) { int[] x = matrix[i]; for (int j = 0; j < x.length; j++) { if (j == 1) { continue; } Color c = new Color(x[j]); r += c.getRed(); g += c.getGreen(); b += c.getBlue(); } } return new Color(r / 8, g / 8, b / 8).getRGB(); } }

获取验证码代码:

private static final String IMG_PATH = "/image/picture/*.*"; private static final String TEMP_IMG_PATH = "/image/temp/temp.png"; private static final Long IMG_CACHE_EX_TIME = 120L; /** * 获取图片,由于spring boot打包成jar之后,获取到获取不到resources里头的图片,对此进行处理 * @param path * @return * @author pangxianhe * @date 2020年1月2日 */ public List queryFileList(String path) { //获取容器资源解析器 ResourcePatternResolver resolver = new PathMatchingResourcePatternResolver(); List filelist = new ArrayList(); // 获取远程服务器IP和端口 try { //获取所有匹配的文件 Resource[] resources = resolver.getResources(path); for(Resource resource : resources) { //获得文件流,因为在jar文件中,不能直接通过文件资源路径拿到文件,但是可以在jar包中拿到文件流 InputStream stream = resource.getInputStream(); String targetFilePath =resource.getFilename(); File ttfFile = new File(targetFilePath); /* if(!ttfFile.getParentFile().exists()) { ttfFile.getParentFile().mkdir(); }*/ FileUtils.copyInputStreamToFile(stream, ttfFile); filelist.add(ttfFile); } }catch (Exception e) { e.printStackTrace(); } return filelist; } /** * 获取验证码 * @return * @throws Exception * @author pangxianhe * @date 2020年1月2日 */ @RequestMapping(value = "/getImageVerifyCode", method = RequestMethod.POST) @ApiOperation(value = "校验ODS账号是否存在", notes = "校验ODS账号是否存在") public Result getImageVerifyCode() throws Exception { // 读取图库目录 List imgList = queryFileList(IMG_PATH); int randNum = new Random().nextInt(imgList.size()); File targetFile = imgList.get(randNum); List tempimgList = queryFileList(TEMP_IMG_PATH); File tempImgFile = tempimgList.get(0); // 根据模板裁剪图片 Map resultMap = VerifyImageUtil.pictureTemplatesCut(tempImgFile, targetFile); int xWidth = (int) resultMap.get("xWidth"); // sessionId 为key,value滑动距离X轴,缓存120秒 // redisTemplate.(session.getId(), xWidth, IMG_CACHE_EX_TIME); String chenckMoveid = IdWorker.get32UUID(); BoundValueOperations redisOper = redisTemplate.boundValueOps(chenckMoveid); redisOper.set(xWidth, IMG_CACHE_EX_TIME, TimeUnit.SECONDS); // 移除map的滑动距离,不返回给前端 resultMap.remove("xWidth"); resultMap.put("chenckMoveid", chenckMoveid); return Result.success().put("data",JSONObject.toJSON(resultMap).toString() ); }

图片保存路径:

登陆验证代码:

// 校验滑块随机数 if (ObjectUtil.isNull(x_index)||ObjectUtil.isNull(chenckMoveid)) { return Result.error("请使用滑块验证码"); } else { // 获取readis缓存的随机数 Object x_index_orld = redisTemplate.opsForValue().get(String.valueOf(chenckMoveid)); if (ObjectUtil.isNull(x_index_orld)) { return Result.error("请刷新滑块验证码"); } else { Double dMoveLength = Double.valueOf(x_index_orld.toString()); Double xWidth = Double.valueOf(x_index); if (Math.abs(xWidth - dMoveLength) > 10) { //验证不通过,这个地方需要注意一下,不管他是否验证通过,都需要重置一下图片和坐标位置,避免暴力破解 redisTemplate.delete(String.valueOf(chenckMoveid)); return Result.error("请拖动到正确的位置"); } else { //验证通过 redisTemplate.delete(String.valueOf(chenckMoveid)); } } }

最后一步,见证奇迹的时刻:

有人说缺少result类,已经补上,其实这个可以根据自己的项目进行封装的。

package com.app.base.kernel.model; import java.util.HashMap; import java.util.Map; import cn.hutool.http.HttpStatus; /** * @FileName Result.java * @Description: 返回数据 * * * */ public class Result extends HashMap { private static final long serialVersionUID = 1L; public Result() { put("code", 20000);// 50008:非法的token; 50012:其他客户端登录了; 50014:Token 过期了;code为非20000是抛错 可结合自己业务进行修改 put("message", "success"); } public static Result error() { return error(HttpStatus.HTTP_INTERNAL_ERROR, "未知异常,请联系管理员"); } public static Result error(String message) { return error(HttpStatus.HTTP_INTERNAL_ERROR, message); } public static Result error(int code, String message) { Result result = new Result(); result.put("code", code); result.put("message", message); return result; } public static Result error(int code, String message, Map map) { Result result = error(code, message); result.putAll(map); return result; } public static Result success(String message) { Result result = new Result(); result.put("message", message); return result; } public static Result success(Map map) { Result result = new Result(); result.putAll(map); return result; } public static Result success() { return new Result(); } public Result put(String key, Object value) { super.put(key, value); return this; } }



【本文地址】


今日新闻


推荐新闻


    CopyRight 2018-2019 办公设备维修网 版权所有 豫ICP备15022753号-3